/*
 *	MidiPlayer.java
 *
 *	This file is part of jsresources.org
 */

/*
 * Copyright (c) 1999 - 2006 by Matthias Pfisterer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

import gnu.getopt.Getopt;

import javax.sound.midi.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


/**
 * <titleabbrev>MidiPlayer</titleabbrev>
 * <title>Playing a MIDI file (advanced)</title>
 * <p/>
 * <formalpara><title>Purpose</title>
 * <para>Plays a single MIDI file. Allows to select the sequencer,
 * the synthesizer or MIDI port or dumping to the console.</para>
 * </formalpara>
 * <p/>
 * <formalpara><title>Usage</title>
 * <para>
 * <cmdsynopsis>
 * <command>java MidiPlayer</command>
 * <arg choice="plain"><option>-l</option></arg>
 * </cmdsynopsis>
 * <cmdsynopsis>
 * <command>java MidiPlayer</command>
 * <arg><option>-s</option></arg>
 * <arg><option>-m</option></arg>
 * <arg><option>-d <replaceable>devicename</replaceable></option></arg>
 * <arg><option>-c</option></arg>
 * <arg><option>-S <replaceable>sequencername</replaceable></option></arg>
 * <arg choice="plain"><replaceable>midifile</replaceable></arg>
 * </cmdsynopsis>
 * </para></formalpara>
 * <p/>
 * <formalpara><title>Parameters</title>
 * <variablelist>
 * <varlistentry>
 * <term><option>-l</option></term>
 * <listitem><para>list the availabe MIDI devices, including sequencers</para></listitem>
 * </varlistentry>
 * <varlistentry>
 * <term><option>-m</option></term>
 * <listitem><para>play on the MIDI port</para></listitem>
 * </varlistentry>
 * <varlistentry>
 * <term><option>-d <replaceable>devicename</replaceable></option></term>
 * <listitem><para>play on the named MIDI device</para></listitem>
 * </varlistentry>
 * <varlistentry>
 * <term><option>-c</option></term>
 * <listitem><para>dump on the console</para></listitem>
 * </varlistentry>
 * <varlistentry>
 * <p/>
 * <term><option>-S <replaceable>sequencername</replaceable></option></term>
 * <listitem><para>play using the named Sequencer</para></listitem>
 * </varlistentry>
 * <varlistentry>
 * <term><option><replaceable>midifile</replaceable></option></term>
 * <listitem><para>the name of the MIDI file that should be
 * played</para></listitem>
 * </varlistentry>
 * </variablelist>
 * <p/>
 * <para>All options may be used together.
 * No option is equal to giving <option>-s</option>.</para>
 * <p/>
 * </formalpara>
 * <p/>
 * <formalpara><title>Bugs, limitations</title>
 * <p/>
 * <para>This example requires the JDK1.5 or later.
 * </para>
 * <p/>
 * </formalpara>
 * <p/>
 * <formalpara><title>Source code</title>
 * <para>
 * <ulink url="MidiPlayer.java.html">MidiPlayer.java</ulink>,
 * <ulink url="DumpReceiver.java.html">DumpReceiver.java</ulink>,
 * <ulink url="MidiCommon.java.html">MidiCommon.java</ulink>,
 * <ulink url="http://www.urbanophile.com/arenn/hacking/download.html">gnu.getopt.Getopt</ulink>
 * </para>
 * </formalpara>
 */
public class MidiPlayer {
    /**
     * Flag for debugging messages.
     * If true, some messages are dumped to the console
     * during operation.
     */
    private static boolean DEBUG = false;
    private static Sequencer sm_sequencer = null;

    /**
     * List of opened MidiDevices.
     * This stores references to all MidiDevices that we've
     * opened except the sequencer.
     * It is used to close them properly on exit.
     */
    private static List sm_openedMidiDeviceList;

    private static boolean sm_bFinished = false;


    public static void main(String[] args) {
        /*
           *	Set when the sequence should be played on the default
           *	internal synthesizer.
           */
        boolean bUseSynthesizer = false;

        /*
           *	Set when the sequence should be played on the default
           *	external MIDI port.
           */
        boolean bUseMidiPort = false;

        /*
           *	Set when the sequence should be played on a MidiDevice
           *	whose name is in strDeviceName. This can be any device,
           *	including internal or external synthesizers, MIDI ports
           *	or even sequencers.
           */
        boolean bUseDevice = false;

        /*
           *	Set when the sequence should be dumped in the console window
           *	(or whereever the standard output is routed to). This gives
           *	detailed information about each MIDI event.
           */
        boolean bUseConsoleDump = false;

        /*
           *	The device name to use when bUseDevice is set.
           */
        String strDeviceName = null;

        /*
           *	The name of the sequencer to use. This is optional. If not
           *	set, the default sequencer is used.
           */
        String strSequencerName = null;

        /*
           *	Parsing of command-line options takes place...
           */
        Getopt g = new Getopt("MidiPlayer", args, "hlsmd:cS:D");
        int c;
        while ((c = g.getopt()) != -1) {
            switch (c) {
                case 'h':
                    printUsageAndExit();

                case 'l':
                    MidiCommon.listDevicesAndExit(false, true);

                case 's':
                    bUseSynthesizer = true;
                    break;

                case 'm':
                    bUseMidiPort = true;
                    break;

                case 'd':
                    bUseDevice = true;
                    strDeviceName = g.getOptarg();
                    if (DEBUG) {
                        out("MidiPlayer.main(): device name: " + strDeviceName);
                    }
                    break;

                case 'c':
                    bUseConsoleDump = true;
                    break;

                case 'S':
                    strSequencerName = g.getOptarg();
                    if (DEBUG) {
                        out("MidiPlayer.main(): sequencer name: " + strSequencerName);
                    }
                    break;

                case 'D':
                    DEBUG = true;
                    break;

                case '?':
                    printUsageAndExit();

                default:
                    out("getopt() returned " + c);
                    break;
            }
        }

        /*
           *	If no destination option is choosen at all,
           *	we default to playing on the internal synthesizer.
           */
        if (!(bUseSynthesizer | bUseMidiPort | bUseDevice | bUseConsoleDump)) {
            if (DEBUG) out("using default synthesizer because no other option was given");
            bUseSynthesizer = true;
        }

        /*
           *	We make shure that there is only one more argument, which
           *	we take as the filename of the MIDI file we want to play.
           */
        String strFilename = null;
        for (int i = g.getOptind(); i < args.length; i++) {
            if (strFilename == null) {
                strFilename = args[i];
            } else {
                printUsageAndExit();
            }
        }
        if (strFilename == null) {
            printUsageAndExit();
        }
        File midiFile = new File(strFilename);

        /*
           * We create a Sequence object from the input file.  This is
           * set later at the Sequencer as its sequence.
           *
           * We create an (File)InputStream and decorate it with a
           * buffered stream. This is set later at the Sequencer as the
           * source of a sequence.

           * There is another programming technique: Creating an
           * (File)InputStream and set this at the sequencer as the
           * source of a sequence. While this technique seems somewhat
           * unnatural, it in fact is more efficient on Sun's
           * implementation of the Java Sound API up to version 1.4.2 of
           * the JDK. Furthermore, it allows playback of RMF files.
           *
           * However, the technique used above should be considered the
           * standard technique. It is especially appropriate if the JDK
           * 1.5.0 or Tritonus is used.
           */
        Sequence sequence = null;
        try {
            if (DEBUG) out("before MIDI file reading.");
            sequence = MidiSystem.getSequence(midiFile);
            if (DEBUG) out("MIDI file read.");
        }
        catch (InvalidMidiDataException e) {
            printExceptionAndExit(e);
        }
        catch (IOException e) {
            printExceptionAndExit(e);
        }

        /*
           *	Now, we need a Sequencer to play the sequence.
           *	In case we have passed a sequencer name on the command line,
           *	we try to get that specific sequencer.
           *	Otherwise, we simply request the default sequencer.
           */
        try {
            if (strSequencerName != null) {
                MidiDevice.Info seqInfo = MidiCommon.getMidiDeviceInfo(strSequencerName, true);
                if (seqInfo == null) {
                    out("Cannot find device " + strSequencerName);
                    System.exit(1);
                }
                sm_sequencer = (Sequencer) MidiSystem.getMidiDevice(seqInfo);
                if (DEBUG) out("Sequencer: " + sm_sequencer);
            } else {
                /* We obtain an unconnected sequencer since we always connect
                     * synthesizers explicitly in the code below.
                     */
                sm_sequencer = MidiSystem.getSequencer(false);
            }
        }
        catch (MidiUnavailableException e) {
            printExceptionAndExit(e);
        }
        if (sm_sequencer == null) {
            out("MidiPlayer.main(): can't get a Sequencer");
            System.exit(1);
        }
        if (DEBUG) out("Sequencer: " + sm_sequencer);

        /*
           *	There is a bug in the Sun jdk1.3/1.4.
           *	It prevents correct termination of the VM.
           *	So we have to exit ourselves.
           *	To accomplish this, we register a Listener to the Sequencer.
           *	It is called when there are "meta" events. Meta event
           *	47 is end of track.
           *
           *	Thanks to Espen Riskedal for finding this trick.
           */
        sm_sequencer.addMetaEventListener(new MetaEventListener() {
            public void meta(MetaMessage event) {
                if (event.getType() == 47) {
                    if (DEBUG) {
                        out("MidiPlayer.<...>.meta(): end of track message received, closing sequencer and attached MidiDevices...");
                    }
                    sm_sequencer.close();
                    Iterator iterator = sm_openedMidiDeviceList.iterator();
                    while (iterator.hasNext()) {
                        MidiDevice device = (MidiDevice) iterator.next();
                        device.close();
                    }
                    if (DEBUG) {
                        out("MidiPlayer.<...>.meta(): ...closed, now exiting");
                    }
                    sm_bFinished = true;
                    //System.exit(0);
                }
            }
        });

        /*
           *	If we are in debug mode, we set additional listeners
           *	to produce interesting (?) debugging output.
           */
        if (DEBUG) {
            sm_sequencer.addMetaEventListener(
                    new MetaEventListener() {
                        public void meta(MetaMessage message) {
                            out("%%% MetaMessage: " + message);
                            out("%%% MetaMessage type: " + message.getType());
                            out("%%% MetaMessage length: " + message.getLength());
                        }
                    });

            int[] anControllers = new int[128];
            for (int i = 0; i < anControllers.length; i++) {
                anControllers[i] = i;
            }
            sm_sequencer.addControllerEventListener(
                    new ControllerEventListener() {
                        public void controlChange(ShortMessage message) {
                            out("%%% ShortMessage: " + message);
                            out("%%% ShortMessage controller: " + message.getData1());
                            out("%%% ShortMessage value: " + message.getData2());
                        }
                    },
                    anControllers);
        }

        /*
           *	The Sequencer is still a dead object.
           *	We have to open() it to become live.
           *	This is necessary to allocate some ressources in
           *	the native part.
           */
        try {
            sm_sequencer.open();
        }
        catch (MidiUnavailableException e) {
            printExceptionAndExit(e);
        }
        if (DEBUG) out("Sequencer opened.");

        /*
           *	Next step is to tell the Sequencer which
           *	Sequence it has to play. In this case, we
           *	set it as the InputStream created above.
           */
        try {
            sm_sequencer.setSequence(sequence);
        }
        catch (InvalidMidiDataException e) {
            printExceptionAndExit(e);
        }
        if (DEBUG) out("Sequence set.");

        /*
           *	Now, we set up the destinations the Sequence should be
           *	played on.
           */
        sm_openedMidiDeviceList = new ArrayList();
        if (bUseSynthesizer) {

            /* For the Sun implementation of Java Sound (up to 1.4.2),
                  the default Sequencer is also a
                  Synthesizer. So to play only on the default
                  Synthesizer, no further actions are
                  required. However, this is
                  implementation-specific behaviour. To write
                  portable programs, it is strongly recommanded
                  to follow the programming technique shown
                  below.
               */
            if (sm_sequencer instanceof Synthesizer) {
                /* Sun implementation; no action required. */
            } else {

                /*
                     *	We try to get the default synthesizer, open()
                     *	it and chain it to the sequencer with a
                     *	Transmitter-Receiver pair.
                     */
                try {
                    Synthesizer synth = MidiSystem.getSynthesizer();
                    synth.open();
                    sm_openedMidiDeviceList.add(synth);
                    Receiver synthReceiver = synth.getReceiver();
                    Transmitter seqTransmitter = sm_sequencer.getTransmitter();
                    seqTransmitter.setReceiver(synthReceiver);
                }
                catch (MidiUnavailableException e) {
                    e.printStackTrace();
                }
            }
        }

        if (bUseMidiPort) {
            /*
                *	We try to get a Receiver which is already
                *	associated with the default MIDI port.
                *	It is then linked to a sequencer's
                *	Transmitter.
                */
            try {
                Receiver midiReceiver = MidiSystem.getReceiver();
                Transmitter midiTransmitter = sm_sequencer.getTransmitter();
                midiTransmitter.setReceiver(midiReceiver);
            }
            catch (MidiUnavailableException e) {
                e.printStackTrace();
            }
        }

        if (bUseDevice) {
            /*	Here, we try to use a MidiDevice as destination
                *	whose name was passed on the command line.
                *	It is then linked to a sequencer's
                *	Transmitter.
                */
            MidiDevice.Info[] aInfos = MidiSystem.getMidiDeviceInfo();
            MidiDevice.Info info = MidiCommon.getMidiDeviceInfo(strDeviceName, true);
            if (info == null) {
                out("Cannot find device " + strDeviceName);
            }
            try {
                MidiDevice midiDevice = MidiSystem.getMidiDevice(info);
                midiDevice.open();
                sm_openedMidiDeviceList.add(midiDevice);
                Receiver midiReceiver = midiDevice.getReceiver();
                Transmitter midiTransmitter = sm_sequencer.getTransmitter();
                midiTransmitter.setReceiver(midiReceiver);
            }
            catch (MidiUnavailableException e) {
                e.printStackTrace();
            }
        }

        if (bUseConsoleDump) {
            /*
                *	We allocate a DumpReceiver object. Its job
                *	is to print information on all received events
                *	to the console.
                *	It is then linked to a sequencer's
                *	Transmitter.
                */
            try {
                Receiver dumpReceiver = new DumpReceiver(System.out);
                Transmitter dumpTransmitter = sm_sequencer.getTransmitter();
                dumpTransmitter.setReceiver(dumpReceiver);
            }
            catch (MidiUnavailableException e) {
                e.printStackTrace();
            }
        }

        /*
           *	Now, we can start over.
           */
        if (DEBUG) {
            out("MidiPlayer.main(): starting sequencer...");
        }
        sm_sequencer.start();
        if (DEBUG) {
            out("MidiPlayer.main(): ...started");
        }
        while (!sm_bFinished) {
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {
                // IGNORE
            }
        }
        // TODO: close devices
    }


    private static void printUsageAndExit() {
        out("MidiPlayer: usage:");
        out("  java MidiPlayer -h");
        out("    gives help information");
        out("  java MidiPlayer -l");
        out("    lists available MIDI devices");
        out("  java MidiPlayer [-s] [-m] [-d <output device name>] [-c] [-S <sequencer name>] [-D] <midifile>");
        out("    -s\tplays on the default synthesizer");
        out("    -m\tplays on the MIDI port");
        out("    -d <output device name>\toutputs to named device (see '-l')");
        out("    -c\tdumps to the console");
        out("    -S <sequencer name>\tuses named sequencer (see '-l')");
        out("    -D\tenables debugging output");
        out("All options may be used together.");
        out("No option is equal to giving -s.");
        System.exit(1);
    }


    /**
     * Handle Exception.
     * In case of an exception, we dump the exception
     * including the stack trace to the console
     * output. Then, we exit the program.
     */
    private static void printExceptionAndExit(Exception e) {
        e.printStackTrace();
        System.exit(1);
    }


    private static void listDevicesAndExit(boolean forInput, boolean forOutput) {
        if (forInput && !forOutput) {
            out("Available MIDI IN Devices:");
        } else if (!forInput && forOutput) {
            out("Available MIDI OUT Devices:");
        } else {
            out("Available MIDI Devices:");
        }

        MidiDevice.Info[] aInfos = MidiSystem.getMidiDeviceInfo();
        for (int i = 0; i < aInfos.length; i++) {
            try {
                MidiDevice device = MidiSystem.getMidiDevice(aInfos[i]);
                boolean bAllowsInput = (device.getMaxTransmitters() != 0);
                boolean bAllowsOutput = (device.getMaxReceivers() != 0);
                if ((bAllowsInput && forInput) || (bAllowsOutput && forOutput)) {
                    out("" + i + "  "
                            + (bAllowsInput ? "IN " : "   ")
                            + (bAllowsOutput ? "OUT " : "    ")
                            + aInfos[i].getName() + ", "
                            + aInfos[i].getVendor() + ", "
                            + aInfos[i].getVersion() + ", "
                            + aInfos[i].getDescription());
                }
            }
            catch (MidiUnavailableException e) {
                // device is obviously not available...
            }
        }
        if (aInfos.length == 0) {
            out("[No devices available]");
        }
        System.exit(0);
    }


    /*
      *	This method tries to return a MidiDevice.Info whose name
      *	matches the passed name. If no matching MidiDevice.Info is
      *	found, null is returned.
      *	If forOutput is true, then only output devices are searched,
      *	otherwise only input devices.
      */
    // TODO: check against MidiCommon
    private static MidiDevice.Info getMidiDeviceInfo(String strDeviceName,
                                                     boolean forOutput) {
        MidiDevice.Info[] aInfos = MidiSystem.getMidiDeviceInfo();
        for (int i = 0; i < aInfos.length; i++) {
            if (aInfos[i].getName().equals(strDeviceName)) {
                try {
                    MidiDevice device = MidiSystem.getMidiDevice(aInfos[i]);
                    boolean bAllowsInput = (device.getMaxTransmitters() != 0);
                    boolean bAllowsOutput = (device.getMaxReceivers() != 0);
                    if ((bAllowsOutput && forOutput) || (bAllowsInput && !forOutput)) {
                        return aInfos[i];
                    }
                } catch (MidiUnavailableException mue) {
                }
            }
        }
        return null;
    }


    private static void out(String strMessage) {
        System.out.println(strMessage);
	}
}



/*** MidiPlayer.java ***/

